Modification Manuelle de l'IAT
Rien ne vaut un peu de pratique pour mieux comprendre. Le programme cible ne contient que deux fonctions MessageBoxA de USER32.dll et ExitProcess de KERNEL32.dll. C'est juste un petit programme Hello Word. On va lui ajouter les éléments suivants:
Oups, avant tout je précise que les fonctions que je vais lui rajouter
je ne les utiliserai pas, c'est pas mon but pour l'instant. L'objectif c'est
seulement de réussir à reconstruire un IAT valide possédant
de nouvelles fonctions. Comment savoir alors si on a réussit où
non ? C'est simple si après votre modification le programme redémarre
correctement, c'est bon. Dans le cas contraire vous n'allez pas tarder à
comprendre que quelque chose va de travers.
- GetModuleHandleA (de KERNEL32.dll)
- FT_Exit8 (de KERNEL32.dll) ------------> Ordinal
- ShellExecuteA (de SHELL32.dll)
Pour ce qui est de "FT_Exit8", on pourrait l'importé normalement,
mais on va plutôt l'importer à partir de son n°Ordinal. Comme
ça on aura 3 cas différents, c'est mieux pour tout comprendre.
A oui au fait son n°Ordinal c'est 10Ah (Voir
SoftIce : -exp)
Un conseil avisé, avant de vous lancer dans l'ajout de fonctions prenez
toujours le temps de rechercher les noms EXACTES des DLL et des fonctions. Parce
que par exemple mettre le nom "messageboxa"
au lieu de "MessageBoxA",
vous obligera à simplement corriger cette petite erreur, mais par contre
mettez un "MessageBox" au lieu d'un "MessageBoxA"
et là vous êtes bon pour décaler tout dans l'IAT et donc
tout réadresser, tout refaire en fait. Et si vous regardez les Ref Win32
aucune fonction n'est présentée avec les extensions AINSI(A) ou
UNICODE(W). Donc un coup de W32Dasm sur un prog ou SoftIce sont plutôt
bien venus avant de passer à l'attaque.
I - ON OBSERVE :
On décortique le programme cible d'abord avec l'excellant ProcDump:
Import Table : 2010
Import Address Table : 2000 (IAT)
Virtual Size | Virtual Offset | Raw Size | Raw Offset | Characteristics | |
.text | 00000028 | 00001000 | 00000200 | 00000400 | 60000020 |
.rdata | 00000092 |
00002000 | 00000200 | 00000600 | 40000040 |
.data | 00000024 | 00003000 | 00000200 | 00000800 | C0000040 |
1 - D'abord on nous dit que la section qui représente l'IAT commence
en 2000h c'est donc .rdata
2 - Le point d'entrée dans cette section est en 2010h. Le point d'entrée
de l'IAT c'est toujours la structure IMAGE_IMPORT_DESCRIPTOR. Ce qui signifie
que le premier DWORD où débute IMAGE_IMPORT_DESCRIPTOR est en
2010h
Maintenant il faut bien se souvenir d'une chose, c'est que W32Dasm ou HexWorkShop
ne se servent pas de RVA (Virtual Offset), ils utilisent un adressage directe
(Raw Offset) Par contre toutes les adresses contenues dans l'IAT sont des RVA.
Ce qui complique notre affaire.
Ca signifie qu'en utilisant HexWorkShop, l'IAT commence en 00000600 (et fini
en 00000800) l'IMAGE_IMPORT_DESCRIPTOR sera donc en 00000610h (Il suffit de
garder le même déplacement, et d'effectuer un changement de base
entre ces deux représentations)
Vue par HexWorkShop:
|
Un peu vide vous trouvez pas.... Enfin c'est une autre histoire quand un programme
possède plus de 150 fonctions au lieu de 2 comme ici.
En 610h on retrouve le point d'entrée dans l'IAT qui est le premier dword de la structure IMAGE_IMPORT_DESCRIPTOR. La taille de cette structure étant de 5 dword, je vous les ai surlignés. Le reste de ce qui est souligné sont aussi des structures IMAGE_IMPORT_DESCRIPTOR.
L'IAT possède toujours autant d'IMAGE_IMPORT_DESCRIPTOR qu'il y a de
DLL importées par votre programme + 1 IMAGE_IMPORT_DESCRIPTOR remplit
de NULL pour notifier le programme de la fin de ce groupe de structures.
Pourquoi y en a autant ? C'est parce que cette structure est justement celle
qui sert à conserver toutes les info à propos d'une DLL donnée.
Souvenez-vous de sa définition :
IMAGE_IMPORT_DESCRIPTOR <>
- OriginalFirstThunk dd ? <------------------------------ C'est un RVA qui
pointe sur la structure ORIGINAL FIRST THUNK
- TimeDateStamp dd ? <---------------------------------- Généralement
mis à 0 comme ici
- ForwarderChain dd ? <----------------------------------- Réservé
(mis à 0FFFFFFFFh si on suit la norme. Mais c'est pas le cas ici)
- Name1 dd ? <----------------------------------------------- RVA pointant
sur le nom de la DLL
- FirstThunk dd ? <------------------------------------------ C'est un RVA
qui pointe sur la structure FIRST THUNK
IMAGE_IMPORT_DESCRIPTOR ENDS
Prenez la première structure IMAGE_IMPORT_DESCRIPTOR, regardez son 4e
DWORD (Name1 donc) ce DWORD vaut 6A20 0000.
Je le remet dans le format Intel ce qui donne 0000 206A. Maintenant
souvenez vous que toutes ces adresses sont des RVA donc elle utilisent comme
base VirtualOffset (2000) alors que HexWorkShop réalise un adressage
à partir du Raw Offset (600) donc 0000 206A représente 0000 066A
dans notre changement de base. Et que trouve t'on en 66Ah,...... la chaine de
caractères USER32.dll (ouhhhoouh.... gagné!!!)
Le premier IMAGE_IMPORT_DESCRIPTOR concerne donc USER32 et ses Fonctions. Le
second concerne KERNEL32 et ses Fonctions vous l'aviez deviné.
Maintenant comment retrouver ses fonctions justement. Normalement c'est le membre
OriginalFirstThunk qui est utilisé mais parfois il peut être NULL
(ce qui normalement ne doit pas arriver) dans ce cas on prend FirstThunk.
Ces deux membres pointent sur deux structures distinctent mais qui contiennent
exactement les même info.
ORIGINAL_FIRST_THUNK et FIRST_THUNK (A oui les structures je les mets toujours en majuscules alors que les membres je les laisse toujours en minuscule, sinon on va pas s'en sortir) Mes deux structures ORINAL_FIRST_THUNK et FIRST_THUNK sont normalement appelées toutes les deux IMAGE_THUNK_DATA mais je n'utiliserai plus ce terme, car si il décrit bien le type de structure à laquelle on a à faire, il est malheureusement peu explicite quant à savoir si on parle des FirstThunk ou bien des OriginalFirstThunk. Donc pour ma part, je les appelle toujours ainsi pour faire la différence, mais en sachant très bien que ce ne sont pas leurs vrais noms.
ORIGINAL_FIRST_THUNK<>
- RVA ImageImportByName1 <-------------------------------------- RVA qui
nous amène à une structure IMAGE_IMPORT_BY_NAME
- ..............
- RVA ImageImportByName n
ORIGINAL_FIRST_THUNK ENDS
FIRST_THUNK est exactement identique.
Pour une DLL donnée, il y aura autant de RVA que cette DLL possède
de fonctions + 1 RVA NULL pour terminer la structure.
|
Reprenons le premier IMAGE_IMPORT_DESCRIPTOR (Taille 5 DWORDs) son premier
DWORD est donc un RVA qui pointe sur une structure ORIGINAL_FIRST_THUNK. 5420
0000 -->0000 2054 --> 0000 000654
En 654h on trouve le DWORD 5C20 0000 suivit d'un DWORD 0000 0000 NULL. Cà
signifie qu'en 654h on rencontre le début de la structure ORIGINAL_FIRST_THUNK
mais qu'elle ne possède qu'un seul membre donc qu'une seule fonction.
Maintenant le contenu de 654 (le contenu de OFT) est lui-même un RVA qui
pointe sur une structure IMAGE_IMPORT_BY_NAME :
IMAGE_IMPORT_BY_NAME STRUCT
Hint dw ? ; <--------------------------------------------------n°ORDINAL
(Word donc taille 2)
Name1 db ? ; <----------------------------------------------Nom de la fonction
IMAGE_IMPORT_BY_NAME ENDS
Le contenu de 654 est 5C20 0000 --> 0000205C --> 0000 065C
Et que trouve-t-on en 65Ch, d'abord le N°ordinal de la fonction sur un WORD,
immédiatement suivit de la fonction correspondante.
|
Maintenant un détail, certaines fonctions sont importées uniquement
By Ordninal (ici elles sont toutes importées By Name) Dans ce cas, elles
n'ont donc aucune structure IMAGE_IMPORT_BY_NAME et le N°Ordinal est donc
placé en lieu et place du RVA de ORIGINAL_FIRST_THUNK (et de FIRST_THUNK
puisqu'ils sont jumeaux)
|
Quelques détailles:
Vous voyez qu'ici le point d'entrée dans l'IAT n'est pas en début
de section, et bien ce n'est pas incompatible du tout, toutes ces structures
peuvent êtres mises dans l'ordre que vous souhaitez. Rien ne vous empèche
de mettre IMAGE_IMPORT_DESCRIPTOR dès le début, et d'ailleurs
je préfère grandement. Il faudra juste aligner la valeur de Import
Table sur celle de Import Address Table (Vue dans ProcDump) Du moment que tous
les RVA contenus dans cette section soient correctement réadressés,
l'IAT restera valide.
Autre chose, si vous regardez bien, la chaine de caractère USER32.dll
et terminée non pas par un byte NULL mais pas deux, idem pour KERNEL32.dll,
et bien un seul aurait suffit, c'est juste que le linker de Masm (ben oui j'ai
créée la cible Hello.exe avec Masm) suit ses propres règles
de construction, mais en mettre un seul ne provoquera pas d'erreur. De même,
Masm construit toujours l'IAT en commençant par FIRST_THUNK, IMAGE_IMPORT_DESCRIPTOR,
ORIGINAL_FIRST_THUNK, IMAGE_IMPORT_BY_NAME, mais d'autres linkers le construisent
autrement. D'ailleurs ce serait un moyen de déterminer avec quel langage
un programme a été écrit.
II - ON AGIT :
On va donc réécrire l'IAT avec nos nouvelles fonctions. Perso,
je préfère mettre les IMAGE_IMPORT_DESCRIPTOR en début
de section.
IMAGE_IMPORT_DESCRIPTOR:
On utilise combien de DLL ? .... 3 (USER32.dll, KERNEL32.dll, et SHELL32.dll)
Ce qui fait 3 IMAGE_IMPORT_DESCRIPTOR + 1 VIDE.
(3+1) * 5 DWORD = 20 DWORD = 80 BYTE
IMAGE_THUNK_DATA (ORIGINAL_FIRST_THUNK + FIRST_THUNK):
KERNEL32.dll (ExitProcess, GetModuleHandleA, FT_Exit8) 3 DWORD + 1 NULL
USER32.dll (MessageBoxA) 1 DWORD + 1 NULL
SHELL32.dll(ShellExecute) 1 DWORD + 1 NULL
soit 8 DWORD (seulement pour ORIGINAL_FIRST_THUNK mais autant pour FIRST_THUNK)
16 DWORD = 84 BYTES
IMAGE_IMPORT_BY_NAME
On compte le nb de caractères +1 pour chaque DLL = 36 BYTES si je ne
me suis pas trompé.
On compte le nb de caractères +1 pour chaque fonction (sauf FT_Exit8
car on a dit qu'on l'importait by ordinal et non pas by name) = 55 BYTES
On compte les n°Ordinaux associés à ces noms de fonction (donc
toujours pas FT_Exit8) 5 fonctions * 1 WORD = 10 BYTES
101 BYTE
IMAGE_IMPORT_DESCRIPTOR va être au début de la section, donc en
600h
ORIGINAL_FIRST_THUNK le suit donc il sera en 600h+(80d
BYTES) = 650h
FIRST_THUNK le suit donc il sera en 650h + (8d*4
BYTES) = 670h
IMAGES_IMPORT_BY_NAME suit en 670h + (8d*4
BYTES) = 690h
Peu importe quelle DLL on écrit en premier, peu importe l'ordre dans
lequel on écrit ses fonctions aussi, et même, on pourrait bien
tout mêlanger.... fonctions et DLL en bordel, tout fonctionnerait encore
du moment que Name1 soit correctement adressé sur le bon nom de DLL,
et que OriginalFirstThunk et FirstThunk soient aussi correctement adressés
pour chaque fonction. Une seule chose, chaque fonction doit garder son propre
n°Ordinal 1WORD devant elle.
Le membre ForwarderChain qui normalement doit être mis à
0FFFFFFFFh est à 0 ici, donc je vais lui remettre sa valeur normale.
C'est en plus un exellent point de repère dans la structure IMAGE_IMPORT_DESCRIPTOR.
Je n'ai pas recherché les N°Ordinaux des fonctions ShellExecuteA
et GetModuleHandleA donc je vais les laisser à 0000, ça ne pose
pas de problème puisque je les importe By Name, mais bon pour être
rigoureux il faudrait les placer quand même. Donc n'oubliez pas de laisser
un WORD NULL devant elles pour que le format reste viable (sinon vous obtiendrez
une structure IMAGE_IMPORT_BY_NAME incorrecte). Tout cette table est réécrite
en commençant par replacer les chaînes de caractères représentant
les noms des DLLs et de leurs fonctions. Ensuite seulement il est possible de
savoir quoi mettre dans le reste de la table.
|
On va se débrouiller à partir de ça.
On peut déjà remettre les RVAs de la structure IMAGE_IMPORT_DESCRIPTOR
qui pointent sur le nom de chaque DLL. Ca c'est directe.
On peut aussi remplir les RVA des structures ORIGINAL_FIRST_THUNK et FIRST_THUNK
qui pointent sur les noms des fonctions (enfin 1 WORD avant à cause du
N°Ordinal) C'est ce que je viens de faire au-dessus.
C'est parti...
|
Comment ça : "d'où il vient le dword 0A01
0080 ???? "
Vous vous souvenez que FT_Exit8 on l'importe By Ordinal et que son n° est
10Ah soit 0000 010Ah mais justement parce que c'est une fonction importée
By Ordinal on le notifie en mettant son MSB à 1 (son byte de poids fort
à 1) [1000.......... binaire soit 8........ hexadécimal]
soit 8000 010Ah et vu au format Intel ça donne bien 0A01 0080 et ce n°Ordinal
remplace le RVA qui pointe normalement sur son nom.
Maintenant, on peut finir de remplir IMAGE_IMPORT_DESCRIPTOR (OriginalFirstThunk
et FirstThunk)
|
Bon, on a un IAT propre, mais pas fonctionnel. On n'a pas redirigé le
membre Import Table, Ben oui on a déplacé IMAGE_IMPORT_DESCRIPTOR
en début de section donc on reprend ProcDump et on donne la même
valeur à Import Table que celle de Import Address Table (2000h)
Voilà, l'IAT est fonctionnel, et pour en être sûr il ne
reste plus qu'à utiliser W32Dasm si il trouve des modules importés
et toutes les fonctions, c'est gagné
+++++++++++++++++++ IMPORTED FUNCTIONS ++++++++++++++++++
Number of Imported Modules = 3 (decimal)
Import Module 001: KERNEL32.dll
Import Module 002: USER32.dll
Import Module 003: SHELL32.dll
+++++++++++++++++++ IMPORT MODULE DETAILS +++++++++++++++
Import Module 001: KERNEL32.dll
Addr:00002090 hint(0075) Name: ExitProcess
Addr:0000209E hint(0000) Name: GetModuleHandle <------------- on lui avait
pas donné de valeur pour son ordinal
Addr:8000010A hint(010A) Name: FT_Exit8 <--------------------- grâce
à son ordinal on a bien retrouvé son nom (8000 ....)
Import Module 002: USER32.dll
Addr:000020BD hint(01BB) Name: MessageBoxA
Import Module 003: SHELL32.dll
Addr:000020D6 hint(0000) Name: ShellExecute <--------------- lui non plus, et pourtant ça marche vous voyez.
AAAAAAHHH !!!!!!!!! LE PROGRAMME NE FONCTIONNE PLUS !!!!!!!!!!!
C'est normal, on a tout chamboulé dans l'IAT, et la section code (.text)
va chercher directemment des info dedans. Ca signifie qu'on à détruit
les relations qui existent entre le code et l'IAT. Reprenez le programme d'origine
tout en bas on trouve deux
FF25......... jump dword ptr [.......]
Ce qui se trouve entre parenthèse c'est le RVA contenu dans le membre
FirstThunk de IMAGE_IMPORT_DESCRIPTOR (+ l'ImageBase)
Autrement dit, c'est le RVA qui pointe sur la structure IMAGE_IMPORT_BY_NAME
des ces fonctions en question.
initialement on avait:
* Reference To: KERNEL32.ExitProcess, Ord:0075h
|
:00401022 FF2500204000 Jmp dword ptr [00402000]
donc 2000h soit 0020
Reprenons notre tableau, ils sont maintenant à :
7020 pour ExitProcess soit 2070--->00402070
7420 pour GetModuleHandleA soit 2074 --->00402074
7820 pour FT_Exit8 soit 2078 ---> 00402078
8020 pour MessageBoxA soit 2080 ---> 00402080
8820 pour ShellExcuteA soit 2088 ---> 00402088
Il faut donc effectuer une petite réparation.
On recherche avec HexWorkShop
Recherche de: | Remplacement par |
FF2508204000 (MessageBoxA) | FF2580204000 |
FF2500204000 (ExitProcess) | FF2570204000 |
Oufff ça Marche!!!
Le programme refonctionne, et on a trois nouvelles fonctions à disposition
dans l'IAT. Il y a deux façons d'appeler les nouvelles fonctions. Soit
vous créez des
[AdresseX] FF25......jmp dword ptr [........] en fin de code et un simple Call
AdresseX appelera votre fonction
Soit vous pouvez l'appeler directement en l'appelant ainsi : Call dword ptr
[.........]
Le contenu du jmp ou du call étant l'ImageBase + FirstThunk bien entendu.
(Mais surtout pas OriginalFirstThunk)